ΦFlow Animation Gallery¶

GitHub   •   Documentation   •   API   •   Demos

Google Collab Book

This notebook shows various animations created with ΦFlow. To animate a plot, simply pass sequence data to vis.plot() and specify the time dimension using the animate argument.

All animations are rendered with Matplotlib and ffmpeg.

In [1]:
# !pip install --quiet phiflow
In [2]:
from phi.flow import *

Line Plots¶

We define the sine waves $\sin(x - t)$ and $\sin(x + t)$ and sample them on a grid from $x=0$ to $x=2\pi$ with resolution $R_x = 100$. This is done for 60 values of $t$, linearly spaced between $0$ and $4\pi$. These curves are animated in the left plot and their sum, a standing wave, is plotted on the right.

In [3]:
curves = CenteredGrid(lambda x, t: stack([math.sin(x - t), math.cos(x + t)], channel('c')), x=100, t=60, bounds=Box(x=2*PI, t=4*PI)).t[:-1]
plot({"Curves": curves, "Sum": sum(curves.c)}, animate='t')
Out[3]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Geometric Primitives¶

Geometric primitives like Sphere and Box can be plotted directly. Instance dimensions denote collections of objects.

In [4]:
plot(Sphere(x=wrap([0, 2], instance('s')), y=0, radius=math.linspace(0, 1, batch(t=50))**.5), animate='t')
Out[4]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>
In [5]:
x = math.range(instance(boxes=10))
plot(Box(x=(x, x+1), y=(0, 2 * math.sin(math.linspace(0, 2*PI, batch(t=30)) + x*.5))), animate='t')
Out[5]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Quiver Plots¶

In addition to the point locations, PointClouds can store per-point values, such as vectors.

In [6]:
x = math.rotate_vector(vec(x=1, y=0), angle=math.linspace(0, 2*PI, spatial(points=50)))
dx, x = x.points[1:] - x.points[:-1], x.points[:-1]
plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')
/tmp/ipykernel_1781/4004326419.py:3: RuntimeWarning: rename_dims() default implementation is slow on large dimensions ((pointsˢ=49)). Please implement __replace_dims__()
  plot(vis.overlay(PointCloud(x, dx), rename_dims(PointCloud(x, dx, color='#40FFFF'), 'points', 'time')), animate='time')
Out[6]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

2D Scalar Noise¶

Here we visualize the built-in class Noise, sampling it on a $64^3$ grid ranging from 0 to 10 along each axis. We plot all $x$-$y$ slices over time, yielding a scanning animation. The left plot shows noise with a smoothness of 1.0 and the right plot shows the same random noise (equal seed) with smoothness of 1.3.

In [7]:
noise = Noise(smoothness=stack({"Default Noise": 1.0, "Smooth Noise": 1.3}, batch('c')))
grid = CenteredGrid(noise, x=64, y=64, z=64, bounds=Box(x=10, y=10, z=10))
plot(grid, animate='z', show_color_bar=False)
Out[7]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Solar System¶

This animation shows two planets circling the sun, using a PointCloud with spherical elements for visualization.

In [8]:
PLANETS = instance(planets='Sun,Earth,Mars')
x = tensor([(0, 0), (9, 0), (0, 12)], PLANETS, channel(vector='x,y'))
x = math.rotate_vector(x, math.linspace(0, wrap([0, 5, 3], PLANETS), batch(time=130)))
plot(PointCloud(Sphere(x, radius=wrap([1, .4, .2], PLANETS))), animate='time')
Out[8]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

3D Voxels¶

Two spheres are placed in a $32^3$ domain, at positions (16, 16, 0) and (16, 16, 32). Their radii grow linearly in time. These spheres are then sampled on a regular grid and plotted as voxels. Additionally, we plot the cross section $y=16$ as a 2D heat map.

In [9]:
sphere = Sphere(x=16, y=16, z=0, radius=math.linspace(0, 16, batch(time=17)))
grid = CenteredGrid(union(sphere, sphere.shifted((0, 0, 32))), x=32, y=32, z=32)
plot({"3D": grid, "2D Slice": grid.y[16]}, animate='time', frame_time=300)
Out[9]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Spirals¶

For these animated spirals, we plot 200 points whose distance increases linearly from the origin and whose angle increases linearly from 0 to $\alpha = 20 \frac{t}{T}$ where $t$ denotes the current frame and $T$ the number of frames. When no geometric shape is specified, PointClouds are plotted as x.

In [10]:
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(0, 20, batch(t=100)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
Out[10]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Varying the parameters can produce vastly different patterns.

In [11]:
dst = math.linspace(0, 1, instance(points=200))
angle = math.linspace(0, math.linspace(PI*200, 1.1*PI*200, batch(t=200)), dst.shape)
plot(PointCloud(dst * vec(x=math.cos(angle), y=math.sin(angle))), animate='t')
Out[11]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Bouncing Balls¶

This demo visualizes the evolution of a PointCloud as a 3D scatter plot animation.

Thirty balls are placed at random locations. The initial velocities are sampled from a normal distribution with standard deviations $\sigma_x = \sigma_y = 1$ and $\sigma_z = 2$.

A simulation is than run for 100 frames, performing the following operations at each step:

  • Gravity is applied, $g_z = -9.81$,
  • Friction is computed proportional to velocity,
  • The balls are advected using Euler integration,
  • When below $z=0$, the y velocity is negated to simulate an elastic collision with the floor.
In [12]:
x0 = math.random_uniform(instance(balls=30), channel(vector='x,y,z')) + 5
balls = PointCloud(Sphere(x0, radius=.1), math.random_normal(x0.shape) * (1, 1, 2))
def step(balls, dt=.1):
  balls *= math.where(balls.points.vector['z'] < 0, (1, 1, -1), 1) * 0.7 ** dt
  return advect.points(balls, balls, dt) + (0, 0, -9.81 * dt)
plot(iterate(step, batch(t=100), balls).mask(), animate='t')
Out[12]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Burgers' Equation¶

Burgers' equation is a partial differential equation consisting of an advection term and a diffusion term acting on a vector field $v$ (velocity). It reads

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x}.$$

Here, we simulate Burgers' equation on a $64^2$ grid for 100 time steps with $\Delta t = 0.5$, starting with a randomly generated initial condition. The evolution is plotted as a vector field. A standalone demo of Burgers' equation is also available here.

In [13]:
velocity = CenteredGrid(Noise(smoothness=1.5, vector='x,y'), extrapolation.PERIODIC, x=64, y=64) * 2
def burgers(v, dt=.5):
    return diffuse.explicit(advect.semi_lagrangian(v, v, dt), .08, dt)
vis.plot(iterate(burgers, batch(time=100), velocity), animate='time')
Out[13]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Incompressible Flow¶

Next, we simulate an incompressible fluid with moderate diffusion. We split the PDE

$$\frac{\partial v}{\partial t} = \nu \frac{\partial^2 v}{\partial x^2} - v \frac{\partial v}{\partial x} - \nabla p \quad \mathrm{s.t.} \quad \nabla \cdot v = 0$$

into advection, diffusion and pressure projection but will rely purely on numerical diffusion in this example. Starting from a random initial conditions, the fluid is simulated for 40 time steps and the vorticity $w = \nabla \times v$ and the pressure $p$ are shown. Also check out the tutorial notebook or the standalone Python scripts.

In [14]:
def incompressible_fluid_step(v: StaggeredGrid, p: CenteredGrid, dt=.5):
    return fluid.make_incompressible(advect.advect(v, v, dt), (), Solve('auto', 1e-5, 1e-5, x0=p))
trj = iterate(incompressible_fluid_step, batch(time=40), *fluid.make_incompressible(StaggeredGrid(Noise(), 0, x=64, y=64)))
plot({"Vorticity": field.curl(trj[0]), "Pressure": trj[1]}, animate='time', same_scale=False)
Out[14]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Pressure Solve¶

The incompressibility constraint $\nabla \cdot v$ in the Navier-Stokes equations is numerically achieved by solving the linear system of equations

$$\nabla p = \nabla \cdot v'$$

which yields the pseudo-pressure $p$. This is typically done with a conjugate gradient solver using a laplace stencil (5-point in 2D, 7-point in 3D). This demo visualizes how the pressure optimization progresses internally for a tentative velocity $v' = (1, 1)$ inside a circle at the center of the $100^2$ domain and $v' = 0$ outside.

In [15]:
with math.SolveTape(record_trajectories=True) as solves:
  fluid.make_incompressible(StaggeredGrid(Sphere(x=50, y=50, radius=20), 0, x=100, y=100))
plot(solves[0].x, animate='trajectory', frame_time=50)
Out[15]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>

Reaction-Diffusion¶

This simulation consists of two quantities $u$ and $v$ that interact via a non-linear partial differential equation (PDE) involving diffusion terms, $\nabla^2 u$ and $\nabla^2 v$. Depending on the exact form and parameters of the PDE, a myriad of resulting patterns can be achieved. The simulation is run for 1000 frames but we only plot every 10th since small time steps must be chosen for stability.

In [16]:
def reaction_diffusion(u, v, du=.19, dv=.05, f=.06, k=.062, dt=1.):
    return u + dt * du * field.laplace(u) - u * v**2 + f * (1 - u), v + dt * dv * field.laplace(v) + u * v**2 - (f + k) * v
trj_u, trj_v = iterate(reaction_diffusion, batch(time=1000), *[CenteredGrid(Noise(scale=20, smoothness=1.3), x=100, y=100) * .2 + .5]*2)
plot(trj_u.time[::10], animate='time')
Out[16]:
Your browser does not support the video tag.
<Figure size 640x480 with 0 Axes>